/**
 * Copyright (c) 2020 - present, Inspur Genersoft Co., Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { DataGridProps } from '../data-grid.props';
import { UseDataView, UseVirtualScroll, UseVisualData, VisualData } from './types';
import { computed, ref, Ref, watch } from 'vue';
import { ColumnRenderContext } from './use-column';

export function useVirtualScroll(
    props: DataGridProps,
    dataViewComposition: UseDataView,
    visibleDatas: Ref<VisualData[]>,
    columnContext: Ref<ColumnRenderContext>,
    visualDataComposition: UseVisualData,
    visibleCapacity: Ref<number>,
    preloadCount: number,
    sidebarWidth: Ref<number>
): UseVirtualScroll {
    const { dataView } = dataViewComposition;
    const { getVisualData, maxVisibleRowIndex, minVisibleRowIndex } = visualDataComposition;
    const gridViewWidth = ref(columnContext.value.primaryColumnsWidth);
    const leftColumnsWidth = ref(columnContext.value.leftColumnsWidth);
    const rightColumnsWidth = ref(columnContext.value.rightColumnsWidth);
    const rowHeight = props.rowOption?.height || 28;
    const offsetY = ref(0);
    const offsetX = ref(0);
    let minViewPortRowIndex = 0;
    let maxViewPortRowIndex = visibleCapacity.value - 1;
    const dataGridWidth = ref(0);
    const viewPortHeight = ref(0);
    const viewPortWidth = ref(0);

    const visibleGridViewHeight = rowHeight * visibleCapacity.value;

    const gridViewHeight = computed(() => {
        return dataView.value.length * rowHeight;
    });

    function getLatestGridViewHeight(): number {
        if (dataView.value.length <= 0) {
            return 0;
        }
        const latestVisibleDataViewItem =
            maxVisibleRowIndex.value < dataView.value.length
                ? dataView.value[maxVisibleRowIndex.value]
                : dataView.value[dataView.value.length - 1];
        const gridViewHeight: number =
            (latestVisibleDataViewItem.__fv_data_position__ || 0) +
            (latestVisibleDataViewItem.__fv_data_height__ || 0) +
            Math.max(dataView.value.length - 1 - maxVisibleRowIndex.value, 0) * rowHeight;
        return gridViewHeight;
    }

    const scrollThumbHeight = computed(() => {
        const latestGridViewHeight = getLatestGridViewHeight();
        return dataView.value.length > visibleCapacity.value + preloadCount
            ? Math.floor(((visibleCapacity.value + preloadCount) / dataView.value.length) * viewPortHeight.value)
            : Math.floor((viewPortHeight.value / Math.max(latestGridViewHeight, viewPortHeight.value)) * viewPortHeight.value);
    });

    const scrollThumbWidth = computed(() => {
        return Math.floor((viewPortWidth.value / Math.max(gridViewWidth.value, viewPortWidth.value)) * dataGridWidth.value);
    });

    const topOfPostion = 0;
    const leftOfPostion = 0;
    const rightOfPosition = computed(() => {
        return dataGridWidth.value - (leftColumnsWidth.value + gridViewWidth.value + rightColumnsWidth.value + sidebarWidth.value);
    });

    function getVisibleRowIndexByPosition(topRowPosition: number): number {
        const matchedRow = dataView.value.find((dataItem) => dataItem.__fv_data_position__ > topRowPosition);
        if (matchedRow) {
            return matchedRow.__fv_index__ - 1;
        }
        return 0;
    }

    function reCalculateVisualDataRows(forceToRefresh?: boolean) {
        if (!dataView.value.length) {
            visibleDatas.value = [];
            return;
        }
        const start = props.pagination?.enable ? 0 : Math.max(minViewPortRowIndex - preloadCount, 0);
        const lastRowIndexInDataView = dataView.value[dataView.value.length - 1].__fv_index__;
        const end = props.pagination?.enable
            ? dataView.value.length - 1
            : Math.min(maxViewPortRowIndex + preloadCount, lastRowIndexInDataView);
        const preDataItem = dataView.value[start - 1];
        const visualData = getVisualData(start, end, preDataItem, forceToRefresh);
        if (visualData.length) {
            visibleDatas.value = [...visualData];
        }
    }

    const shouldShowVirticalScrollbar = computed(() => {
        const latestGridViewHeight = getLatestGridViewHeight();
        return latestGridViewHeight > viewPortHeight.value;
    });

    const shouldShowHorizontalScrollbar = computed(() => {
        return gridViewWidth.value > viewPortWidth.value;
    });

    function updateOffsetY(deltaY: number) {
        let offsetYValue = offsetY.value + deltaY;
        if (offsetYValue > topOfPostion) {
            offsetYValue = topOfPostion;
        }
        const onTopOfScrollbar = offsetYValue > topOfPostion;
        const latestGridViewHeight = getLatestGridViewHeight();
        const shouldShowScrollBar = shouldShowVirticalScrollbar.value;
        if (shouldShowScrollBar && offsetYValue < viewPortHeight.value - latestGridViewHeight) {
            offsetYValue = viewPortHeight.value - latestGridViewHeight;
        }
        if (!shouldShowScrollBar || onTopOfScrollbar) {
            offsetYValue = topOfPostion;
        }
        if (offsetY.value !== offsetYValue) {
            offsetY.value = offsetYValue;
            minViewPortRowIndex = getVisibleRowIndexByPosition(Math.abs(offsetY.value));
            maxViewPortRowIndex = minViewPortRowIndex + visibleCapacity.value;
            if (minViewPortRowIndex < minVisibleRowIndex.value || maxViewPortRowIndex > maxVisibleRowIndex.value - preloadCount / 2) {
                reCalculateVisualDataRows();
            }
        }
    }

    function updateOffsetX(deltaX: number) {
        if (!shouldShowHorizontalScrollbar.value) {
            offsetX.value = 0;
            return;
        }

        let offsetXValue = offsetX.value + deltaX;
        if (offsetXValue > leftOfPostion) {
            offsetXValue = leftOfPostion;
        }
        if (offsetXValue < rightOfPosition.value) {
            offsetXValue = rightOfPosition.value;
        }
        if (offsetX.value !== offsetXValue) {
            offsetX.value = offsetXValue;
        }
    }

    function fitHorizontalScroll() {
        if (Math.abs(offsetX.value) + viewPortWidth.value > gridViewWidth.value) {
            offsetX.value = Math.min(viewPortWidth.value - gridViewWidth.value, 0);
        }
    }

    function resetScroll() {
        offsetY.value = 0;
        minViewPortRowIndex = 0;
        maxViewPortRowIndex = visibleCapacity.value - 1;
        reCalculateVisualDataRows();
    }

    watch(viewPortWidth, () => {
        updateOffsetX(0);
    });

    watch(columnContext, () => {
        gridViewWidth.value = columnContext.value.primaryColumnsWidth;
        leftColumnsWidth.value = columnContext.value.leftColumnsWidth;
        rightColumnsWidth.value = columnContext.value.rightColumnsWidth;
    });

    function onWheel(payload: WheelEvent) {
        payload.preventDefault();
        payload.stopPropagation();
        const deltaY = ((payload as any).wheelDeltaY || payload.deltaY) / 10;
        const deltaX = ((payload as any).wheelDeltaX || payload.deltaX) / 10;

        updateOffsetY(deltaY);
        updateOffsetX(deltaX);
    }

    const leftFixedGridDataStyle = computed(() => {
        const styleObject = {
            height: `${dataView.value.length * rowHeight}px`,
            width: `${leftColumnsWidth.value}px`,
            transform: `translateY(${offsetY.value}px)`
        } as Record<string, any>;
        return styleObject;
    });

    const leftFixedGridMergedDataStyle = computed(() => {
        const styleObject = {
            transform: `translateY(${offsetY.value}px)`
        } as Record<string, any>;
        return styleObject;
    });

    const gridDataStyle = computed(() => {
        const styleObject = {
            height: `${dataView.value.length * rowHeight}px`,
            width: `${gridViewWidth.value}px`,
            transform: `translate(${offsetX.value}px, ${offsetY.value}px)`
        } as Record<string, any>;
        return styleObject;
    });

    const gridMergedDataStyle = computed(() => {
        const styleObject = {
            transform: `translate(${offsetX.value}px, ${offsetY.value}px)`
        } as Record<string, any>;
        return styleObject;
    });

    const rightFixedGridDataStyle = computed(() => {
        const styleObject = {
            height: `${dataView.value.length * rowHeight}px`,
            width: `${rightColumnsWidth.value}px`,
            transform: `translateY(${offsetY.value}px)`
        } as Record<string, any>;
        return styleObject;
    });

    const rightFixedGridMergedDataStyle = computed(() => {
        const styleObject = {
            transform: `translateY(${offsetY.value}px)`
        } as Record<string, any>;
        return styleObject;
    });

    const gridSideStyle = computed(() => {
        const styleObject = {
            height: `${dataView.value.length * rowHeight}px`,
            width: `${sidebarWidth.value}px`,
            transform: `translateY(${offsetY.value}px)`
        } as Record<string, any>;
        return styleObject;
    });

    const leftFixedGridHeaderColumnsStyle = computed(() => {
        const styleObject = {
            width: `${leftColumnsWidth.value}px`
        } as Record<string, any>;
        return styleObject;
    });

    const gridHeaderColumnsStyle = computed(() => {
        const styleObject = {
            transform: `translateX(${offsetX.value}px)`
        } as Record<string, any>;
        return styleObject;
    });

    const rightFixedGridHeaderColumnsStyle = computed(() => {
        const styleObject = {
            width: `${rightColumnsWidth.value}px`
        } as Record<string, any>;
        return styleObject;
    });

    const thumbPositionY = computed(() => {
        const latestGridViewHeight = getLatestGridViewHeight();
        return (offsetY.value / (viewPortHeight.value - latestGridViewHeight)) * (viewPortHeight.value - scrollThumbHeight.value);
    });

    const verticalScrollThumbStyle = computed(() => {
        const styleObject = {
            height: `${scrollThumbHeight.value}px`,
            top: `${thumbPositionY.value}px`
        } as Record<string, any>;
        return styleObject;
    });

    const thumbPositionX = computed(() => {
        return (offsetX.value / rightOfPosition.value) * (dataGridWidth.value - scrollThumbWidth.value);
    });

    const horizontalScrollThumbStyle = computed(() => {
        const styleObject = {
            width: `${scrollThumbWidth.value}px`,
            left: `${thumbPositionX.value}px`
        } as Record<string, any>;
        return styleObject;
    });

    const onDraggingScrollThumb = ref(false);
    const onDraggingStartY = ref(-1);
    const onDraggingStartX = ref(-1);
    let gridContentElement: any;

    function onDragHorizontalScroll($event: MouseEvent) {
        if (onDraggingScrollThumb.value && onDraggingStartX.value !== -1) {
            const mouseOffset = onDraggingStartX.value - $event.pageX;
            const mouseMoveRange = dataGridWidth.value - scrollThumbWidth.value;
            if (Math.abs(mouseOffset) <= mouseMoveRange) {
                const deltaOffsetX =
                    (mouseOffset / (viewPortWidth.value - scrollThumbWidth.value)) * (gridViewWidth.value - viewPortWidth.value);
                updateOffsetX(deltaOffsetX);
                onDraggingStartX.value = $event.pageX;
            }
        }
    }

    function onDragVerticalScroll($event: MouseEvent) {
        if (onDraggingScrollThumb.value && onDraggingStartY.value !== -1) {
            const mouseOffset = onDraggingStartY.value - $event.pageY;
            const mouseMoveRange = visibleGridViewHeight - scrollThumbHeight.value;
            if (Math.abs(mouseOffset) <= mouseMoveRange) {
                const deltaOffsetY =
                    (mouseOffset / (visibleGridViewHeight - scrollThumbHeight.value)) * (gridViewHeight.value - visibleGridViewHeight);
                updateOffsetY(deltaOffsetY);
                onDraggingStartY.value = $event.pageY;
            }
        }
    }

    function releaseDragScroll($event: MouseEvent) {
        onDraggingScrollThumb.value = false;
        onDraggingStartY.value = -1;
        onDraggingStartX.value = -1;
        document.removeEventListener('mouseup', releaseDragScroll);
        if (gridContentElement) {
            gridContentElement.removeEventListener('mousemove', onDragHorizontalScroll);
            gridContentElement.removeEventListener('mousemove', onDragVerticalScroll);
        }
        document.body.style.userSelect = '';
    }

    function onMouseDownScrollThumb($event: MouseEvent, gridContentRef: Ref<any>, thumbType: 'vertical' | 'horizontal') {
        onDraggingScrollThumb.value = true;
        const draggingHandler = thumbType === 'horizontal' ? onDragHorizontalScroll : onDragVerticalScroll;
        if (thumbType === 'vertical') {
            onDraggingStartY.value = $event.pageY;
        }
        if (thumbType === 'horizontal') {
            onDraggingStartX.value = $event.pageX;
        }
        if (gridContentRef.value) {
            gridContentRef.value.addEventListener('mousemove', draggingHandler);
            gridContentElement = gridContentRef.value;
            document.addEventListener('mouseup', releaseDragScroll);
            document.body.style.userSelect = 'none';
        }
    }

    return {
        onMouseDownScrollThumb,
        onWheel,
        dataGridWidth,
        gridDataStyle,
        gridHeaderColumnsStyle,
        gridMergedDataStyle,
        gridSideStyle,
        fitHorizontalScroll,
        viewPortWidth,
        viewPortHeight,
        horizontalScrollThumbStyle,
        leftFixedGridDataStyle,
        leftFixedGridHeaderColumnsStyle,
        leftFixedGridMergedDataStyle,
        reCalculateVisualDataRows,
        resetScroll,
        rightFixedGridDataStyle,
        rightFixedGridHeaderColumnsStyle,
        rightFixedGridMergedDataStyle,
        shouldShowHorizontalScrollbar,
        shouldShowVirticalScrollbar,
        verticalScrollThumbStyle,
        offsetX,
        offsetY
    };
}
